1. 理解属性这一概念

使用 @property 语法来定义对象中所封装的数据。
通过 assign strong weak copy unsafe_unretained定义存储数据所需的正确语义。
开发 iOS 程序时应该使用 nonatomic 属性,因为 atomic 属性会严重影响性能。

2. 在对象内部尽量直接访问实例变量

在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应该通过属性来写,以确保其声明的内存管理语意。
在初始化和 dealloc 方法中,总是应该直接通过实例变量来读取数据。
在使用惰性初始化时,需要通过属性来读取数据。

3. 理解 对象等同性 这一概念。

如果想检测对象的等同性,可提供 NSObjcet协议中的 isEqualhash 方法。
相同的对象一定拥有相同的哈希码,但是两个哈希码相同的对象却未必相同。
不要盲目选择逐个检查每条属性,而是应该依照具体需求来定制方案,比如可以使用唯一的 identifier 来对比检查两个对象是否相等。
编写 hash 方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Person 类
@interface EOCPerson: NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
// 实现 isEqual 方法
- (BOOL)isEqual:(id)object {
// 如果指针相等,则其均指向同一对象
if (self == object) {
return YES;
}
// 判断两个对象所属类是否相同
if ([self class] != [object class]) {
return NO;
}
// 可能存在父类与子类进行对比
EOCPerson *otherPerson = (EOCPerson *)object;
// 检查每个属性是否相等。
if (![_firstName isEqualToString: otherPerson.firstName]) {
return NO;
}
if (![_lastName isEqualToString: otherPerson.lastName]) {
return NO;
}
if (_age != otherPerson.age) {
return NO;
}
return YES;
}
// 这种写法如果将对象放入集合中就会产生性能问题。
// 因为集合类型需要检索哈希表,会用对象的哈希码做索引。
- (NSUInteger)hash {
return 1337;
}
// 这种写法需要额外负担创建字符串开销。如果将对象放入集合,需要先计算哈希码。
- (NSUInteger)hash {
NSString *stringToHash = [NSString stringWithFormat: @"%@:%@:%i", _firstName, _lastName, _age];
return [stringToHash hash];
}
// 既能保持较高效率,也不会过于频繁的重复。
// 在编写 hash 方法时, 应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂度之间取舍。
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUinteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}

3. 以 类族模式 隐藏实现细节

用以隐藏 抽象基类 的实现细节。 比如 UIButton 类,若想创建按钮,需要调用下面这个类方法:

1
2
// 该方法返回的对象都继承自同一个基类 UIButton
+ (UIButton *)buttonWithType:(UIButtonType)type;

举例演示如何创建类族,假设有一个处理员工的类,每个员工都有名字和薪水两个属性,犹豫每个员工的工作内容不同,项目经理在带领员工做项目时,无需关心每个人如何完成工作,仅需指示其开工即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper = 0;
EOCEmployeeTypeDesigner;
EOCEmployeeTypeFinance;
}
// 定义抽象基类
@interface EOCEmployee : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
// 创建员工对象
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
- (void)doADayWork;
@end
@implementation EOCEmployee
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeDeveloper new];
break;
case EOCEmployeeTypeDesigner:
return [EOCEmployeeTypeDesigner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeTypeFinance new];
break;
}
}
- (void)doADaysWork {
// 因为 OC 中没有方法指明某个基类是抽象的
// 可以选择在基类的方法中抛出异常,避免外界调用。
}
@end
// 定义子类
@interface EOCEmployeeDeveloper : EOCEmployee
@end
@implementation EOCEmployeeDeveloper
- (void)doADaysWord {
[self writeCode];
}
@end

如果对象所属的类位于某个类族,那么在查询其类型时要格外当心。

1
2
3
// 看上去会返回 YES,但实际返回 NO
// 因为 employee 并非 Employee 类的实例,而是其子类的实例。
[employee isMemberOfClass: [EOCEmployee class]]

正确判断对象所属类是否位于某个类族时,应该使用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
id maybeAnArray = /* ... */;
// 正确
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
}
// 错误
if ([maybeAnArray class] == [NSArray class]) {
/*
永远不会执行,因为 NSArray 是个类族
[maybeAnArray class] 所返回的类不是 NSArray 类本地
因为 NSArray 的初始化方法所返回的那个实例是隐藏在类族公共接口后面的某个内部类型。
*/
}
```
#### 4. 在既有类中使用关联对象存放自定义数据
可以通过关联对象机制将两个对象链接起来。
定义关联对象时可指定内存管理语义,用以模仿定义属性时采用的拥有关系和非拥有关系。
只有再其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的 bug。
管理关联对象的方法:
```objc
// 以给定的键值和策略对某对象设置关联对象值。
void objc_setAssociatedObject(id object, void*key, id value, objec_AssociationPolicy policy)
//根据给定的键从某对象中获取响应关联对象的值
id objc_getAssociatedObject(id object, void*key)
//移除指定对象的全部关联对象
void objc_removeAssociatedObjects(id object)

关联对象时内存管理的语义表:

关联类型 等效的 @Property 属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

举例演示应用场景,比如 UIAlertView 类, 当用户点击按钮关闭视图时需要用 delegate 来处理动作。但代码分作两部分,读起来有点乱。所以可以使用关联对象的特性整合代码。(举例,并不推荐直接应用).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"Question" message: @"What do you want to do?" delegate: self, cancelButtonTitle: @"Cancel" otherButtonTitles:@"Continue", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
};
objc_setAssociateObject(alert, EOCMyAlertViewKey, block, BJC_ASSOCIATION_COPY);
[alert show];
}
// UIAlertView Delegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex: (NSInteger) buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView EOCMyAlertViewKey);
block(buttonIndex);
}

5. 理解 objc_megSend 的作用

传递消息是 OC 中经常使用的功能,消息有 name 或 selector 可以接受参数,而且可以能还有返回值。
由于 Objective-C 是 C 的超集,所以要先理解 C 语言的函数调用方式, C 语言使用静态绑定,也就是说,在编译期就能决定运行时所应调用的函数,比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void doTheThing(int type) {
if (type == 0) {
printHello();
} else {
printGoodbye();
}
return 0;
}

编译器在编译代码的时候就已经知道程序中有 printHello 和 printGoodbye 这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。但如果写成这样:

1
2
3
4
5
6
7
8
9
10
void doTheThing(int type) {
void (*fnc)();
if (type == 0) {
fnc = printHello;
} else {
fnc = printGoodbye;
}
fnc();
return 0;
}

这时就得使用动态绑定了,所调用的函数直到运行时才能确定。OC 如果要向某个对象传递消息就会使用动态绑定的机制来决定需要调用的方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// someObject -> 消息接受者
// messageName -> selector
// parameter -> 参数
id returnValue = [someObject messageName:parameter];
// 其背后的 C 语言函数叫做 objc_msgSend
void objc_msgSend(id self, SEL cmd, ...)
// 转换之后
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
// 对于一些特殊情况,会调用不同的 msgSend 函数
// 如果待发送的消息要返回结构体,那么由此函数处理,仅当 CPU 寄存器能够容纳下该消息返回类型时执行。
objc_msgSend_stret
// 如果消息返回类型为浮点数,那么由此函数处理,为了解决类似 X86 架构 CPU 中某些令人奇怪的状况。
objc_msgSend_fpret
// 如果给超类发送消息,比如 [super class] 由此函数处理
// 需要了解 super 仅是编辑器的标识符,并不是超类的指针
// 调用 [super class] 仍然是向当前类发送消息。
objc_msgSendSuper

消息由接受者、selector 及参数构成,给某个对象发送消息也就是相当于在该对象上调用方法。
发送给某对象的全部消息都要由动态消息派发系统来处理,该系统查找对应的方法,并执行相应的代码。
每个类都拥有一张表格,其中的指针都指向方法实现的函数,
selector 就是查表时使用的 key
objc_msgSend 等函数就是通过这张表格来寻找应该执行的方法并跳转至其实现。

6. 理解消息转发机制

当对象在收到无法解读的消息时就会启动消息转发机制,我们可由此过程告诉对象应该如何处理未知消息。

若对象无法响应某个 selector,则进入消息转发流程。
通过 runtime 的动态方法解析功能,我们可以在需要用到某个方法时再将其加入到类中。
对象可以把其无法解读的某个消息转交给其他对象处理。

消息转发分为两个阶段:

  1. 第一阶段:询问接受者,所属的类,看其是否动态添加了方法 -> 动态方法解析。
  2. 第二阶段:如果第一阶段执行结束,接受者就无法再用动态新增方法的手段来响应未知 selector 了。首先会询问接受者看看有没有其他对象能够处理这条消息,如果有,转发给那个对象,消息转发结束。如果没有备选的消息接受者,则启动完整的消息转发机制,将消息有关的细节封装到 NSInvocation 中,再给接受者最后一次机会,设法解决当前还未处理的消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 对象在无法解读消息后,首先调用其所属类的这个方法
/*
* return 表示这个类是否能新增一个实例方法处理 SEL
* 参数 selector: 未知的 selector
*/
// 处理实例方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
// 处理类方法
+ (BOOL)resloveClassMethod:(SEL)selector
// 2. 当前接受者还有第二次机会来处理未知 selector
// 询问对象能不能将消息转发给其他接受者处理
/*
* 在对象内部,可能还有一系列其他对象,该对象可经由此方法将
* 能够处理某 selector 的相关内部对象返回。
* return: 如果可以找到备选的接受者,将其返回,否则返回 nil
* 参数 selector: 未知的 selector
*/
- (id)forwardingTargetForSelector:(SEL)selector
// 3. 如果进入这一步,唯一能做的就是启动完整消息转发机制。
/*
* 这个方法基本和第二步的实现方法等效,所以应该场景可能是:
在触发消息前,以某种方式改变消息内容,比如追加另外一个参数,等。
*/
- (void)forwardInvocation:(NSInvocation *)invocation

消息转发全流程图:

接受者在每一步均有机会处理消息,步骤越往后,处理消息的代价就越大。如果在第一步处理最好,这样的话,runtime 就可以将此方法缓存起来,之后该实例再接受到同样的 Selector 就无需启动转发流程了。若想在第三步转发消息,不如提前到第二部,就省去了创建 NSInvocation 的系统开销。

我们来看完整的例子,说明消息转发机制的意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@interface EOCAutoDictionary : NSObject
@property (nonatomic, copy) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>
@interface EOCAutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end
@implementation EOCAutoDictionary
// 禁止编译器生成实例变量及存取方法
@dynamic string, number, date, opaqueObject;
- (id) init {
if (self = [super init]) {
_backingStore = [NSMutableDictionary new];
}
return self;
}
// 取方法函数
id autoDictionaryGetter(id self, SEL _cmd) {
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
NSString *key = NSStringFromSelector(_cmd);
return [backingStore objectForKey:key];
}
// 存方法函数
void autoDictionarySetter(id self, SEL _cmd, id value) {
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
// 删除最后的 ':'
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
// 删除 'set' 前缀
[key deleteCharactersInRange:NSMakeRange(0, 3)];
// 第一位小写
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
// 动态添加实例方法
+(BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self,
sel,
(IMP)autoDictionarySetter,
"v@:@");
} else {
class_addMethod(self,
sel,
(IMP)autoDictionaryGetter,
"@@:");
}
return YES;
}
@end
// 测试
EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSinceNow:475372800];
NSLog(@"dict.date = %@", dict.date);
// output: dict.date = 2032-03-14 11:01:57 +0000

其他属性的访问方法与 date 类似, 要想添加新属性,只需要用 @property 来定义,并将其声明为 @dynamic 即可。

7. 方法替换 (method swizzling)

类的方法列表会把 selector 的名称映射到相关的方法实现上,使得动态消息派发系统据此找到应该调用的方法。这些方法均以函数指针的形式来表示,即 IMP, 其原型:

1
id (* IMP)(id, SEL, ...)

在运行时,可以向类中新增或者替换 selector 所对应的方法实现。
使用另一份实现来代替原有的方法实现,开发者常使用此技术向原有实现中添加新功能。
一般来说,只有调试程序时才需要修改方法实现,不宜滥用。

NSString 为例,它可以响应 lowercaseStringuppercaseStringcapitalizedString 等方法,映射表如下:

而我们说的方法替换就是利用几个 runtime 函数来操作这张表,比如改成这样子:

上图中新增了 newSelector 方法,并交换了 lowercaseString 与 uppercaseString 的实现指针。

1
2
3
4
5
6
7
8
9
10
11
12
// 实现交换两个方法实现的函数
void method_exchangeImplementations(Method m1, Method m2);
// 获取方法实现的函数
Method class_getInstanceMethod(Class aClass, SEL aSelector);
// 实现上图中交换两个方法实现的代码
Method originalMethod = class_getInstanceMethod([NSString class],
@selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class],
@selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

当然,实际应用中像这样直接交换两个方法实现意义不大。但是可以通过这个手段为即有方法实现添加新功能。下面的示例演示如果为 NSStringlowercaseString 方法添加 Debug 的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 为 NSString 添加分类
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end
#import "NSString+EOCMyAdditions.h"
#import <objc/runtime.h>
@implementation NSString (EOCMyAdditions)
// 交换方法
+(void)load {
Method originalMethod = class_getInstanceMethod([NSString class],
@selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class],
@selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
}
// 看上去会陷入递归调用死循环,但运行时方法会进行交换。
- (NSString *)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
@end
// 测试
NSString *string = @"This iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: This iS tHe StRiNg => this is the string

8. 理解 类对象 的用意

每个实例都有一个指向 Class 对象的指针(isa), 用来表示其类型,而这些 Class 对象则构成了类的继承体系。
如果对象类型无法在编译器决定,那么就应该使用类型信息查询方法来探知。
尽量使用类型信息查询方法来确定对象类型,不要直接用 == 比较类对象,因为某些对象可能实现了消息转发。
描述 Objective-C 对象所用的数据结构定义在 runtime 库中的头文件中,其中 id 类型定义如下:

1
2
3
typedef struct objc_object {
Class isa;
} *id;

每个对象结构体的首个成员就是 Class 类的变量,叫做 “is a” 指针, 该变量定义了对象所属的类。比如下面代码

1
NSString *string = @"This is a string";

其中的 “is a” 指针指向的就是 NSString, Class 对象的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_lit *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}

此结构体存放类的元数据, 当然这个结构体的首个变量也是 isa 指针, 说明 Class 本身也是 Objective-C 对象。结构体中还有一个叫做 super_class 的变量,它定义了本类的超类。类对象所属的类型 (也就是 isa 指针指向的类型) 是另外一个类,叫做 元类,用来描述类对象本身所具备的元数据。类方法就定义在此处。假如有一个名为 SomeClass 的子类继承自 NSObject,其继承体系如下图:

super_class 指针确定了继承关系,而 isa 指针描述了实例所属的类。这是我们在查询类继承体系时的参照标准。

我们使用 isMemberOfClass 能够判断出对象是否为某个特定类的实例, 而 isKindOfClass 则能判断出对象是否为某类或者其派生类的实例。

1
2
3
4
5
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass: [NSDictionary class]]; // = NO
[dict isMemberOfClass: [NSMutableDictionary class]] // = YES
[dict isKindOfClass: [NSDictionary class]]; // = YES
[dict isKindOfClass: [NSArray class]]; // = NO

像这样的类型查询方法使用 isa 指针获取对象所属的类,然后通过 super_class 指针在继承体系中游走。比如要查询某个集合类型中元素的类型,通常得到的结果是 id,如果想知道具体类型,就需要使用上面方法。假如我们需要根据数据中存储的对象来生成字符串,就是下列的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (NSString *)commaSeparatedStringFromObjects: (NSArray *)array {
NSMutableString *string = [NSMutableString new];
for (id object in array) {
if ([object isKindOfClass: [NSString class]]) {
[string appendFormat:@"%@,", object];
} else if ([object isKindOfClass: [NSNumber class]]) {
[string appendFormat:@"%d," ,[object intValue]];
} else if ([object isKindOfClass: [NSData class]]) {
NSString *base64Encoded = /* base64 encoded data */;
[string appendFormat:@"%@,", base64Encoded];
} else {
// 不支持的类型
}
}
return string;
}